package risoe.syslab.fhnew.gui;

/*
 * Copyright (c) 2012-2014, Technical University of Denmark (DTU)
 * All rights reserved.
 * 
 * The Flexhouse 2.0 platform ("this software") is licensed under the
 * BSD 3-clause license.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *   - Redistributions of source code must retain the above copyright
 *     notice, this list of conditions and the following disclaimer.
 *   - Redistributions in binary form must reproduce the above copyright
 *     notice, this list of conditions and the following disclaimer in the
 *     documentation and/or other materials provided with the distribution.
 *   - Neither the name of DTU nor the names of its contributors may be used to
 *     endorse or promote products derived from this software without specific
 *     prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE TECHNICAL UNIVERSITY OF DENMARK BE LIABLE FOR ANY
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

import java.awt.BorderLayout;
import java.awt.Font;
import java.awt.FontFormatException;
import java.awt.GraphicsDevice;
import java.awt.GraphicsEnvironment;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.rmi.NoSuchObjectException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.rmi.server.UnicastRemoteObject;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

import javax.swing.JFrame;
import javax.swing.JTabbedPane;
import javax.swing.SwingUtilities;

import org.w3c.dom.svg.SVGDocument;

import risoe.syslab.fhnew.common.SystemConstants;
import risoe.syslab.fhnew.config.PlatformConfig;
import risoe.syslab.fhnew.core.FlexhouseCore;

public class UIViewer implements UIViewerRemoteInterface{
	// Constants
	private static final int HEARTBEAT_FREQUENCY = 5000;
	private static final int RECONNECTION_TIMEOUT = 5; // Seconds
	private static String UI_MANAGER_NAME = null;

	private UIManagerRemoteInterface mManagerInterface;
	private List<String> mTabList;
	{
		mTabList = new ArrayList<String>();
		// Load fonts
		GraphicsEnvironment local = GraphicsEnvironment.getLocalGraphicsEnvironment();
		// Load all fonts in media/fonts
		File fontFolder = new File("media/fonts");
		if (fontFolder.exists()){
			for (File fontFile : fontFolder.listFiles()){
				try {
					Font font = Font.createFont(Font.TRUETYPE_FONT, fontFile);
					// Register this new font with the local GraphicsEnvironment
					local.registerFont(font.deriveFont(12.0f));
				} 
				catch (FontFormatException e) {} 
				catch (IOException e) {}
			}
		}
	}
	private int mVisualizationIndex = -1;
	private boolean mConnected = false;

	// Graphics
	private JFrame mJFrame;
	private JTabbedPane mJTabbedPane;
	{
		mJFrame = new JFrame();
		mJTabbedPane = new JTabbedPane(JTabbedPane.LEFT);
		mJTabbedPane.setTabLayoutPolicy(JTabbedPane.SCROLL_TAB_LAYOUT);
	}

	// Reconnection
	private int mReconnectAttempts = 0;
	private Future<?> mFutureTask = null;
	private final ScheduledExecutorService mExecutor = Executors.newSingleThreadScheduledExecutor();
	private final Runnable mHeartbeatTask = new Runnable() {
		@Override
		public void run() {
			try {
				// Poll and reschedule if successful
				mManagerInterface.issueHeartBeat();
				mExecutor.schedule(mHeartbeatTask,HEARTBEAT_FREQUENCY, TimeUnit.MILLISECONDS);
			} catch (RemoteException e) {
				onDisconnected();
			}
		}
	};
	private final Runnable mReconnectTask = new Runnable() {
		@Override
		public void run() {
			try {
				mReconnectAttempts++;
				System.out.println("Connection Attempt #" +mReconnectAttempts);

				// Start up the RMI interface for this remote viewer
				Registry registry = LocateRegistry.getRegistry(SystemConstants.DEFAULT_RMIPORT);
				UIViewerRemoteInterface stub = (UIViewerRemoteInterface)UnicastRemoteObject.exportObject(UIViewer.this, 0);
				registry.rebind(UI_MANAGER_NAME + System.currentTimeMillis(), stub);

				// Obtain the current visualizations from the UI Manager
				mManagerInterface = (UIManagerRemoteInterface) registry.lookup(UI_MANAGER_NAME);
				
				// Clear all tabs
				mTabList.clear();
				SwingUtilities.invokeAndWait(new Runnable() {
					public void run() {
						mJTabbedPane.removeAll();
					};
				});

				// Obtain visualization from UIManager
				mManagerInterface.addRemoteViewer(UIViewer.this);

				System.out.println("Succesfully connected after "+mReconnectAttempts+" attempts.");
				mReconnectAttempts = 0;

				mConnected = true;

				// Start heartbeat mechanism
				mExecutor.schedule(mHeartbeatTask,HEARTBEAT_FREQUENCY, TimeUnit.MILLISECONDS);
			} catch (Exception e) {
				// Unexport the object and try again after a timeout
				try {
					UnicastRemoteObject.unexportObject(UIViewer.this, true);
				} catch (NoSuchObjectException ex) {
					// Ignore this error.
				}
			} 

			if (!mConnected){
				// Try again in five seconds
				//				System.out.println("Disconnected from platform");
				mExecutor.schedule(mReconnectTask, RECONNECTION_TIMEOUT, TimeUnit.SECONDS);
			}
		}
	};

	public static void main(String[] args) throws UnknownHostException {
		String configFile = args.length > 0? args[0]: null;
		UI_MANAGER_NAME = new PlatformConfig(configFile).getParameter(UIManager.UI_MANAGER_KEY, UIManager.UI_MANAGER_DEFAULT_NAME);
		
		final UIViewer viewer = new UIViewer();
		Runtime.getRuntime().addShutdownHook(new Thread() {
			public void run() {
				viewer.mExecutor.shutdownNow();
				System.out.println("Shutting down visualization viewer");
			}
		});
	}

	private void onDisconnected(){
		mConnected = false;
		mManagerInterface = null;
		try {
			UnicastRemoteObject.unexportObject(this, true);
		} catch (NoSuchObjectException e) {
			// Ignore this error.
			System.out.println(e.getMessage());
		}

		// Check if posting this task can be accomplished
		if (!mExecutor.isShutdown()){


			// Check that no previous task is already scheduled
			if (mFutureTask.isDone())
				// Schedule new reconnection task
				mFutureTask = mExecutor.schedule(mReconnectTask, 1, TimeUnit.SECONDS);
		}
	}

	public UIViewer() {		
		// Start up local graphics on graphics thread
		try {
			SwingUtilities.invokeAndWait(new Runnable() {

				@Override
				public void run() {
					mJTabbedPane.addMouseListener(new MouseListener() {
						@Override
						public void mouseReleased(final MouseEvent e) {
							SwingUtilities.invokeLater(new Runnable() {

								@Override
								public void run() {
									if (!mConnected) return;

									// Check if the user clicked on of the tabs. A return value of -1 indicates no tab.
									int index = mJTabbedPane.indexAtLocation(e.getPoint().x, e.getPoint().y);
									if (index != -1 && index != mVisualizationIndex){
										String name = mJTabbedPane.getToolTipTextAt(index);
										try {
											SVGDocument doc = DocumentUtils.createDocument(mManagerInterface.getSerializedDocument(name));
											if (doc != null){
												// Swap tab content by unloading previously showing Visualization and loading the new one
												Visualization oldVis = (Visualization) mJTabbedPane.getComponentAt(mVisualizationIndex);
												oldVis.unload();
												Visualization newVis = (Visualization) mJTabbedPane.getComponentAt(index);
												newVis.load(doc);

												mVisualizationIndex = index;
												mJTabbedPane.setSelectedIndex(mVisualizationIndex);
											}
										} catch (RemoteException e1) {
											// Disconnect
											onDisconnected();
										}

									}
								}
							});
						}
						@Override
						public void mousePressed(MouseEvent e) {}
						@Override
						public void mouseExited(MouseEvent e) {}
						@Override
						public void mouseEntered(MouseEvent e) {}
						@Override
						public void mouseClicked(MouseEvent e) {}
					});

					mJFrame.setLayout(new BorderLayout());
					mJFrame.setResizable(false);
					mJFrame.setUndecorated(true);

					// Setup environment
					GraphicsEnvironment env = GraphicsEnvironment.getLocalGraphicsEnvironment();
					GraphicsDevice dev = env.getDefaultScreenDevice();
					dev.setFullScreenWindow(mJFrame);

					// Show
					mJFrame.add(mJTabbedPane);
					mJFrame.setVisible(true);
				}
			});
		} catch (InterruptedException e) {
			e.printStackTrace();
		} catch (InvocationTargetException e) {
			e.printStackTrace();
		}

		// Attempt to start up connection
		if (!mExecutor.isShutdown()){
			mFutureTask = mExecutor.submit(mReconnectTask);
		}
	}

	@Override
	public void addVisualization(final String name, final String[] subscribedObjects) {
		int index = -1;
		synchronized (mTabList) {
			mTabList.add(name);
			index = mTabList.size() - 1;
		}

		final int newIndex = index;
		// Update graphics on UI thread
		SwingUtilities.invokeLater(new Runnable() {
			@Override
			public void run() {
				boolean changeVis = false;
				Visualization vis = new Visualization(name, subscribedObjects, UIViewer.this);
				if (mVisualizationIndex == -1){
					changeVis = true;
					try {
						SVGDocument doc = DocumentUtils.createDocument(mManagerInterface.getSerializedDocument(name));
						if (doc != null){
							mVisualizationIndex = newIndex;
							vis.load(doc);
						}
					} catch (RemoteException e) {
						// Disconnect
						onDisconnected();
					}
				}
				mJTabbedPane.add("<html><body leftmargin=15 topmargin=15 marginwidth=15 marginheight=15>"+name+"</body></html>", vis);
				mJTabbedPane.setToolTipTextAt(newIndex, name);

				// Check if current visualization should be changed
				if (changeVis){
					mJTabbedPane.setSelectedIndex(mVisualizationIndex);
				}
			}
		});
	}

	@Override
	public void removeVisualization(final String name) throws RemoteException {
		// Find and remove the visualization from list
		int index = -1;
		synchronized (mTabList) {
			Iterator<String> iterator = mTabList.iterator();
			while (iterator.hasNext()){
				index++;
				if (iterator.next().equals(name)){
					iterator.remove();
					break;
				}
			}
		}

		// Check if the visualization was removed
		if (index != -1){
			final int removedIndex = index;

			// Update graphics from UI thread
			SwingUtilities.invokeLater(new Runnable() {
				@Override
				public void run() {
					// Check if current visualization is the one shutting down
					if (removedIndex == mVisualizationIndex){
						Visualization vis = (Visualization) mJTabbedPane.getComponentAt(removedIndex);
						vis.unload();
						SVGDocument doc;
						try {
							synchronized (mTabList) {
								String newName = mTabList.get(0);
								doc = DocumentUtils.createDocument(mManagerInterface.getSerializedDocument(newName));
								if (doc != null){
									Visualization newVis = (Visualization) mJTabbedPane.getComponentAt(0);
									if (newVis != null){
										newVis.load(doc);
										mVisualizationIndex = 0;
									}
								}
							}
						} catch (RemoteException e) {
							// Disconnect
							onDisconnected();
						}
					}

					// Remove tab from viewer and change to index 0 if the current visualization was just removed
					mJTabbedPane.remove(removedIndex);
					mJTabbedPane.setSelectedIndex(mVisualizationIndex);
				}
			});
		}
	}

	@Override
	public void updateVisualization(final VisualizationUpdate update)
			throws RemoteException {
		if (update != null){
			SwingUtilities.invokeLater(new Runnable(){

				@Override
				public void run(){
					synchronized (mTabList) {
						for (int i=0; i<mTabList.size(); i++){
							if (mTabList.get(i).equals(update.getDestination())){
								Visualization vis = (Visualization) mJTabbedPane.getComponentAt(i);
								vis.executeVisualizationCommand(update);
								break;
							}
						}
					}
				}
			});
		}
	}

	/**
	 * Called by one of the attached visualizations when an event has occurred (click, drag, etc.)
	 * @param event a VisualizationEvent describing the event that has occurred
	 */
	public void onVisualizationEvent(VisualizationEvent event){
		if (event != null){
			if (!mConnected) return;

			try {
				mManagerInterface.onVisualizationEvent(event);
			} catch (RemoteException e) {
				// Disconnect
				onDisconnected();
			}
		}
	}

	@Override
	public boolean issueHeartBeat() throws RemoteException {
		// Signal that this UIViewer is alive
		return true;
	}
}
